package gamemodel;

import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

public class GameModel {

	private ScoreBoard scoreboard; 
	private transient PositionManager pmTeamA;
	private transient PositionManager pmTeamB; 
	private static final int SPECTATOR_POSITION = -500;
	
	private Ball ball; 
	private HashMap<String, Player> playerList; //used to acquire player state when sending update to clients
	private transient ArrayList<String> nameList; //used to efficiently acquire player names to obtain their respective objects during collision checks 
	
	//moving down increases Y, and moving right increases X
	private static final Map<String, Coordinate> playground = new HashMap<String, Coordinate>();
	private static final Map<Integer, List<GoalPost>> goalPostMap = new HashMap<Integer, List<GoalPost>>();
	static {
		//add field boundaries to static playground map 
		playground.put(Playground.TOP_LEFT, new Coordinate(Playground.FIELD_MIN_X,Playground.FIELD_MIN_Y)); 
		playground.put(Playground.BOTTOM_LEFT, new Coordinate(Playground.FIELD_MIN_X,Playground.FIELD_MAX_Y)); 
		playground.put(Playground.TOP_RIGHT, new Coordinate(Playground.FIELD_MAX_X, Playground.FIELD_MIN_Y)); 
		playground.put(Playground.BOTTOM_RIGHT,  new Coordinate(Playground.FIELD_MAX_X, Playground.FIELD_MAX_Y));
		
		//add goal posts to static goal region map
		//--goal post for team A 
		LinkedList<GoalPost> list = new LinkedList<GoalPost>(); 
		list.add(new GoalPost(Playground.GOALPOST_MIN_X, Playground.GOALPOST_MIN_Y));
		list.add(new GoalPost(Playground.GOALPOST_MIN_X, Playground.GOALPOST_MAX_Y)); 
		goalPostMap.put(new Integer(ScoreBoard.TEAM_A), list); 
		
		//--goal post for team B
		list = new LinkedList<GoalPost>(); 
		list.add(new GoalPost(Playground.GOALPOST_MAX_X, Playground.GOALPOST_MIN_Y));
		list.add(new GoalPost(Playground.GOALPOST_MAX_X, Playground.GOALPOST_MAX_Y)); 
		goalPostMap.put(new Integer(ScoreBoard.TEAM_B), list); 
		
	}
	 
	public GameModel(int minute, int maxScore){
		this.scoreboard = new ScoreBoard(minute, maxScore); 
		this.ball = new Ball(Playground.FIELD_MAX_X / 2, Playground.FIELD_MAX_Y / 2); 
		this.playerList = new HashMap<String, Player>(); 
		this.nameList = new ArrayList<String>(); 
		this.pmTeamA = new PositionManager(Playground.FIELD_MAX_Y / 2, Player.RADIUS); 
		this.pmTeamB = new PositionManager(Playground.FIELD_MAX_Y / 2, Player.RADIUS); 
	}
	
	public void addPlayer(String name, int team){
		//error checking
		if (team != ScoreBoard.TEAM_A && team != ScoreBoard.TEAM_B && team != ScoreBoard.TEAM_SPECTATOR){
			return ; 
		}
		
		Player p ; 
		
		if (team == ScoreBoard.TEAM_A){
			p = new Player(name, Playground.RESET_MIN_POSITION_X, this.pmTeamA.computeValue(), team); 
		} else if (team == ScoreBoard.TEAM_B){
			p = new Player(name, Playground.RESET_MAX_POSITION_X, this.pmTeamB.computeValue(), team);  
		} else {
			p = new Player(name, SPECTATOR_POSITION, SPECTATOR_POSITION, team);
		}
		
		this.ball.addObserver(p); 
		playerList.put(name, p); 
		nameList.add(name);
	}
	
	public void removePlayer(String name){
		//error checking
		Player p = playerList.get(name); 
		if (p == null){
			return;
		}
		
		this.ball.deleteObserver(p); 
		playerList.remove(name); 
		nameList.remove(name);
	}
	
	public void changeTeam(String name, int team){
		//error checking
		Player p = playerList.get(name); 
		if (p == null){
			return;
		}
		
		//error checking
		if (team != ScoreBoard.TEAM_A && team != ScoreBoard.TEAM_B && team != ScoreBoard.TEAM_SPECTATOR){
			return; 
		}
		
		int length = this.nameList.size();
		int oldTeam = p.getTeam(); 
		
		//make sure team change is different from current team 
		if (oldTeam == team){
			return; 
		}
		
		p.setTeam(team); 
		
		//reset player positions		
		this.pmTeamA.reset();
		this.pmTeamB.reset(); 
		
		for (int i = 0; i < length; i++){
			p = this.playerList.get(this.nameList.get(i));
			if (p.getTeam() == ScoreBoard.TEAM_A){
				p.setPosition(Playground.RESET_MIN_POSITION_X, this.pmTeamA.computeValue());
				p.setVelocity(0, 0); 
			} else if (p.getTeam() == ScoreBoard.TEAM_B){
				p.setPosition(Playground.RESET_MAX_POSITION_X, this.pmTeamB.computeValue()); 
				p.setVelocity(0, 0);
			} else {
				//only enters scenario if it is spectator
				p.setPosition(SPECTATOR_POSITION, SPECTATOR_POSITION);
				p.setVelocity(0, 0);
			}
		}
	}
	
	public void changeMaxScore(int score){
		//error checking 
		if (score <= 0){
			return; 
		}
		
		this.scoreboard.setMaxScore(score);
	}
	
	public void changeTimeLimit(int minutes){
		//error checking 
		if (minutes <= 0){
			return;
		}
		
		this.scoreboard.setTimeLimit(minutes);
	}
	
	private void checkCollision(){
		int length = this.nameList.size();
		
		for (int i = 0; i < length; i++){
			
			//if spectator skip the collision check
			if (this.playerList.get(nameList.get(i)).getTeam() == ScoreBoard.TEAM_SPECTATOR){
				continue;
			}
			
			//check player - wall collision
			checkWallCollision(this.playerList.get(nameList.get(i))); 
			
			//check if player - ball collision
			if (isColliding(this.playerList.get(nameList.get(i)), ball)){
				resolveCollision(this.playerList.get(nameList.get(i)), ball); 
			}
			
			//check if goal is scored and update scoreboard
			if (isGoalScored()){
				if (this.scoreboard.getState() == ScoreBoard.IN_PROGRESS){
					resetField(); 
				}
				return;
			}
			
			//check if ball - wall collision
			checkWallCollision(ball); 
			
			//check if goal is scored and update scoreboard
			if (isGoalScored()){
				if (this.scoreboard.getState() == ScoreBoard.IN_PROGRESS){
					resetField(); 
				}
				return;
			}
			
			//check if player - goal post collision + ball - goal post collision
			List<GoalPost> list = goalPostMap.get(new Integer(ScoreBoard.TEAM_A)); 
			for (GoalPost gp: list){
				if (isColliding(this.playerList.get(nameList.get(i)), gp)){
					resolveCollision(this.playerList.get(nameList.get(i)), gp); 
				}
				
				if (isColliding(this.ball, gp)){
					resolveCollision(this.ball, gp); 
					
					//check if goal is scored and update scoreboard
					if (isGoalScored()){
						if (this.scoreboard.getState() == ScoreBoard.IN_PROGRESS){
							resetField(); 
						}
						return;
					}
				}
			}
			list = goalPostMap.get(new Integer(ScoreBoard.TEAM_B)); 
			for (GoalPost gp: list){
				if (isColliding(this.playerList.get(nameList.get(i)), gp)){
					resolveCollision(this.playerList.get(nameList.get(i)), gp); 
				}
				
				if (isColliding(this.ball, gp)){
					resolveCollision(this.ball, gp); 
					
					//check if goal is scored and update scoreboard
					if (isGoalScored()){
						if (this.scoreboard.getState() == ScoreBoard.IN_PROGRESS){
							resetField(); 
						}
						return;
					}
				}
			}
			
			for (int j=i+1; j < length; j++){
				
				//if spectator skip the collision check
				if (this.playerList.get(nameList.get(j)).getTeam() == ScoreBoard.TEAM_SPECTATOR){
					continue;
				}
				
				//check if players - player collision
				if (isColliding(this.playerList.get(nameList.get(i)), this.playerList.get(nameList.get(j)))){
					resolvePlayerCollision(this.playerList.get(nameList.get(i)), this.playerList.get(nameList.get(j))); 
				}
				
			}
		}
	}
	
	public static boolean isColliding(Collidable o1, Collidable o2){
		//error checking
		if (o1 == null || o2 == null){
			return false; 
		}
		
		float dx = o1.getPosition().getX() - o2.getPosition().getX(); 
		float dy = o1.getPosition().getY() - o2.getPosition().getY(); 
		
		float sumRadius = o1.getRadius() + o2.getRadius(); 
		float sqrRadius = sumRadius * sumRadius; 
		
		float distSqr = (dx * dx) + (dy * dy); 
		
		if (distSqr <= sqrRadius){
			return true; 
		}
		
		return false; 
	}
	
	private void resolvePlayerCollision(Collidable o1, Collidable o2){
		//error checking 
		if (o1 == null || o2 == null){
			return ; 
		}
		
		//gets the minimum translation distance after intersection
		Vector2d delta = o1.getPosition().subtract(o2.getPosition()); 
		float d = delta.getLength(); 
		Vector2d mtd = delta.multiply(((o1.getRadius() + o2.getRadius())-d)/d);
		
		//push-pull them based on their mass 
		o1.setPosition(o1.getPosition().add(mtd));
	    o2.setPosition(o2.getPosition().subtract(mtd));
	    
	    o1.setVelocity(new Vector2d(0,0)); 
	    o2.setVelocity(new Vector2d(0,0)); 
	}
	
	private void resolveCollision(Collidable o1, Collidable o2){
		//error checking 
		if (o1 == null || o2 == null){
			return ; 
		}
		
		//gets the minimum translation distance after intersection
		Vector2d delta = o1.getPosition().subtract(o2.getPosition()); 
		float d = delta.getLength(); 
		Vector2d mtd = delta.multiply(((o1.getRadius() + o2.getRadius())-d)/d);
		
		//resolve intersection -- inverse mass quantities
		float im1 = 1/o1.getMass(); 
		float im2 = 1/o2.getMass(); 
		
		//push-pull them based on their mass 
		o1.setPosition(o1.getPosition().add(mtd.multiply(im1 / (im1 + im2))));
	    o2.setPosition(o2.getPosition().subtract(mtd.multiply(im2 / (im1 + im2))));
	    
	    //impact speed 
	    Vector2d v = (o1.getVelocity().subtract(o2.getVelocity()));
	    float vn = v.dot(mtd.normalize());
	    
	    // sphere intersecting but moving away from each other already
	    if (vn > 0.0f) return;

	    // collision impulse
	    float i = (-(1.0f + Collidable.restitution) * vn) / (im1 + im2);
	    Vector2d impulse = mtd.multiply(i);
	    
	    // change in momentum		
	    o1.setVelocity(o1.getVelocity().add(impulse.multiply(im1)));
	    o2.setVelocity(o2.getVelocity().subtract(impulse.multiply(im2)));
	}
	
	public static void checkWallCollision(Collidable c){
		if (c.getPosition().getX() - c.getRadius() < 0){ //left wall collision
			c.getPosition().setX(c.getRadius()); 
			c.getVelocity().setX(-(c.getVelocity().getX() * Collidable.restitution));
			c.getVelocity().setY(c.getVelocity().getY() * Collidable.restitution); 
			
		} else if (c.getPosition().getX() + c.getRadius() > Playground.FIELD_MAX_X){ //right wall collision
			c.getPosition().setX(Playground.FIELD_MAX_X - c.getRadius()); 
			c.getVelocity().setX(-(c.getVelocity().getX() * Collidable.restitution)); 
			c.getVelocity().setY(c.getVelocity().getY() * Collidable.restitution); 
			
		} else if (c.getPosition().getY() - c.getRadius() < 0){ //top wall collision
			c.getPosition().setY(c.getRadius()); 
			c.getVelocity().setY(-(c.getVelocity().getY() * Collidable.restitution)); 
			c.getVelocity().setX(c.getVelocity().getX() * Collidable.restitution); 
			
		} else if (c.getPosition().getY() + c.getRadius() > Playground.FIELD_MAX_Y){ //bottom wall collision
			c.getPosition().setY(Playground.FIELD_MAX_Y - c.getRadius()); 
			c.getVelocity().setY(-(c.getVelocity().getY() * Collidable.restitution)); 
			c.getVelocity().setX(c.getVelocity().getX() * Collidable.restitution); 
			
		}
	}
	
	public boolean isGameOver(){
		int state = this.scoreboard.getState(); 
		if (state == ScoreBoard.IN_PROGRESS){
			return false; 
		} else if (state == ScoreBoard.TEAM_A_WIN || state == ScoreBoard.TEAM_B_WIN || state == ScoreBoard.TIME_EXPIRED){
			return true;
		}
		
		return false; 
	}
	
	public void performMove(String playerName, float dx, float dy){
		//error check
		if (isGameOver()){
			return; 
		}
		Player p; 
		//check that the player exists
		if ((p = playerList.get(playerName)) != null ){
			p.performMove(dx, dy); 
			checkCollision(); 
		}
	}
	
	public void performKick(String playerName){
		//error check
		if (isGameOver()){
			return; 
		}
		
		Player p ; 
		//check that the player exists 
		if ((p = playerList.get(playerName)) != null){
			p.performKick(); 
			checkCollision(); 
		}
	}
	
	/**
	 * To be used by gameEngine to determine the reason behind the game over 
	 * @return
	 */
	public int getScoreBoardState(){
		return this.scoreboard.getState();
	}
	
	public String getGameState(){
		//construct ta json object to send to clients 
		return null;
	}
	
	public void performBallMove(){
		if (isGameOver()){
			return;
		}
		this.ball.performMoveHelper();
	}
	
	/**
	 * Determines if a goal has been scored, and updates scoreboard if there is a goal. 
	 * @return true if a goal is scored. False otherwise. 
	 */
	private boolean isGoalScored(){
		float x = this.ball.getPosition().getX(); 
		float y = this.ball.getPosition().getY(); 
		
		//check which net ball is in 
		if ((x - Ball.RADIUS <= Playground.GOALPOST_MIN_X) && y > Playground.GOALPOST_MIN_Y && y < Playground.GOALPOST_MAX_Y){
			this.scoreboard.addScore(ScoreBoard.TEAM_B); 
			return true;
		} else if ((x + Ball.RADIUS >= Playground.GOALPOST_MAX_X) && y > Playground.GOALPOST_MIN_Y && y < Playground.GOALPOST_MAX_Y){
			this.scoreboard.addScore(ScoreBoard.TEAM_A); 
			return true; 
		}
		
		return false; 
	}
	
	public void resetGame(int minute, int maxScore){
		//reset scoreboard
		this.scoreboard.reset(minute, maxScore); 
		
		//reset players + ball
		this.resetField(); 
	}
	
	/**
	 * Resets the ball and players to their original locations
	 */
	public void resetField(){
		int length = this.nameList.size(); 
		
		//reset ball position
		this.ball.setPosition(Playground.FIELD_MAX_X / 2, Playground.FIELD_MAX_Y / 2);
		this.ball.setVelocity(0, 0); 
		
		//reset player positions
		this.pmTeamA.reset(); 
		this.pmTeamB.reset();
		Player p ; 
		
		for (int i = 0; i < length; i++){
			p = this.playerList.get(this.nameList.get(i)); 
			if (p.getTeam() == ScoreBoard.TEAM_A){
				p.setPosition(Playground.RESET_MIN_POSITION_X, this.pmTeamA.computeValue());
				p.setVelocity(0, 0); 
			} else if (p.getTeam() == ScoreBoard.TEAM_B){
				p.setPosition(Playground.RESET_MAX_POSITION_X, this.pmTeamB.computeValue()); 
				p.setVelocity(0, 0);
			} else {
				//no need to recompute spectator values as they should have never changed
				continue; 
			}
		}
		
	}
	
	public void tick(){
		this.scoreboard.tick();
	}
	
	public void draw(Graphics2D g){
		//draw scoreboard
		this.scoreboard.paint(g);
		
		//draw wall boundaries
		g.setColor(Color.orange); 
		g.draw(new Line2D.Float(playground.get(Playground.TOP_LEFT).getX(), playground.get(Playground.TOP_LEFT).getY(), playground.get(Playground.TOP_RIGHT).getX(), playground.get(Playground.TOP_RIGHT).getY()));
		g.draw(new Line2D.Float(playground.get(Playground.TOP_LEFT).getX(), playground.get(Playground.TOP_LEFT).getY(), playground.get(Playground.BOTTOM_LEFT).getX(), playground.get(Playground.BOTTOM_LEFT).getY()));
		g.draw(new Line2D.Float(playground.get(Playground.TOP_RIGHT).getX(), playground.get(Playground.TOP_RIGHT).getY(), playground.get(Playground.BOTTOM_RIGHT).getX(), playground.get(Playground.BOTTOM_RIGHT).getY()));
		g.draw(new Line2D.Float(playground.get(Playground.BOTTOM_LEFT).getX(), playground.get(Playground.BOTTOM_LEFT).getY(), playground.get(Playground.BOTTOM_RIGHT).getX(), playground.get(Playground.BOTTOM_RIGHT).getY()));
		
		// draw goal posts
		g.setColor(Color.GREEN); 
		List<GoalPost> l = goalPostMap.get(ScoreBoard.TEAM_A); 
		for (GoalPost gp : l){
			g.fill(new Ellipse2D.Float(gp.getPosition().getX() - GoalPost.RADIUS, gp.getPosition().getY() - GoalPost.RADIUS, gp.getRadius() * 2, gp.getRadius() * 2)); 
		}
		l = goalPostMap.get(ScoreBoard.TEAM_B); 
		for (GoalPost gp : l){
			g.fill(new Ellipse2D.Float(gp.getPosition().getX() - GoalPost.RADIUS, gp.getPosition().getY() - GoalPost.RADIUS, gp.getRadius() * 2, gp.getRadius() * 2)); 
		}
		
		//draw ball
		ball.paint(g); 
		
		//draw players
		Collection<Player> c = playerList.values();  
		for (Player p: c){
			p.paint(g);	
		}
	}
}
